/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.primitives.stdlib;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Properties;
import java.util.jar.JarFile;

import fr.x9c.cadmium.kernel.AbstractNativeRunner;
import fr.x9c.cadmium.kernel.Block;
import fr.x9c.cadmium.kernel.CadmiumException;
import fr.x9c.cadmium.kernel.CodeRunner;
import fr.x9c.cadmium.kernel.Context;
import fr.x9c.cadmium.kernel.Fail;
import fr.x9c.cadmium.kernel.Fatal;
import fr.x9c.cadmium.kernel.Primitive;
import fr.x9c.cadmium.kernel.PrimitiveProvider;
import fr.x9c.cadmium.kernel.Value;
import fr.x9c.cadmium.util.CustomClassLoader;

/**
 * Implements all primitives from 'natdynlink.c'.
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.2
 * @since 1.2
 */
@PrimitiveProvider
public final class NatDynlink {

    /**
     * No instance of this class.
     */
    private NatDynlink() {
    } // end empty constructor

    /**
     * Returns the list of all the units declared by the program.
     * @param ctxt context
     * @param unit ignored
     * @return the list of all the units declared by the program
     */
    @Primitive
    public static Value caml_natdynlink_getmap(final CodeRunner ctxt,
                                               final Value unit)
        throws Fatal.Exception {
        final InputStream is =
            ctxt.getClass().getResourceAsStream("cafesterolMain.gmap");
        if (is != null) {
            final Block b = Block.createString(readBytes(is));
            return Value.createFromBlock(b);
        } else {
            Fatal.raise("Fatal error: globals map is missing\n(probably due to a mismatch between Cadmium and Cafesterol versions).");
            return Value.UNIT; // never reached
        } // end if/else
    } // end method 'caml_natdynlink_getmap(CodeRunner, Value)'

    /**
     * Returns the count of initialized globals.
     * @param ctxt context
     * @param unit ignored
     * @return the count of initialized globals
     */
    @Primitive
    public static Value caml_natdynlink_globals_inited(final CodeRunner ctxt,
                                                       final Value unit) {
        final int res = ((AbstractNativeRunner) ctxt).getGlobalsInited();
        return Value.createFromLong(res);
    } // end method 'caml_natdynlink_globals_inited(CodeRunner, Value)'

    /**
     * Opens a plugin.
     * @param ctxt context
     * @param filename plugin to load
     * @param global whether the symbols of the loaded plugin are global
     * @return an handle to the plugin
     */
    @Primitive
    public static Value caml_natdynlink_open(final CodeRunner ctxt,
                                             final Value filename,
                                             final Value global) {
        final Context c = ctxt.getContext();
        final File file = c.getRealFile(filename);
        try {
            final URL url = file.toURI().toURL();
            final URLClassLoader loader =
                new URLClassLoader(new URL[] { url },
                                   CustomClassLoader.INSTANCE);
            final JarFile jar = new JarFile(file);
            final byte[] header = readBytes(jar.getInputStream(jar.getEntry("/PluginHeader")));
            final Properties props = new Properties();
            props.load(jar.getInputStream(jar.getEntry("/PluginMembers")));
            final Block handle = Block.createBlock(Block.ABSTRACT_TAG,
                                                   Value.UNIT);
            handle.setCustom(new Handle(loader, jar, props));
            final Block data = Block.createString(header);
            final Block b = Block.createBlock(0,
                                              Value.createFromBlock(handle),
                                              Value.createFromBlock(data));
            return Value.createFromBlock(b);
        } catch (final Throwable t) {
            final Block b = Block.createString("not an OCaml plugin");
            return Value.createFromBlock(b);
        } // end try/catch
    } // end method 'caml_natdynlink_open(CodeRunner, Value, Value)'

    /**
     * Runs the code related to the 'entry point' of a module.
     * @param ctxt context
     * @param handle plugin handle
     * @param symbol module name
     * @return <i>unit</i>
     */
    @Primitive
    public static Value caml_natdynlink_run(final CodeRunner ctxt,
                                            final Value handle,
                                            final Value symbol)
        throws Fail.Exception {
        final String s = symbol.asBlock().asString();
        final Handle h = (Handle) handle.asBlock().asCustom();
        if (!s.equals("_shared_startup")) {
            final AbstractNativeRunner runner = (AbstractNativeRunner) ctxt;
            final String additionalClasses = h.properties.getProperty(s + "*");
            if (additionalClasses != null) {
                for (String name : additionalClasses.split(",")) {
                    executeEntry(runner, h.classLoader, name.trim());
                } // end for
            } // end if
            final String className = h.properties.getProperty(s).trim();
            return executeEntry(runner, h.classLoader, className);
        } else {
            return Value.UNIT;
        } // end if/else
    } // end method 'caml_natdynlink_run(CodeRunner, Value, Value)'

    /**
     * No implementation of this primitive..
     * @param ctxt context
     * @param handle ignored
     * @param symbol ignored
     * @return <i>unit</i>
     * @throws Fail.Exception always
     */
    @Primitive
    public static Value caml_natdynlink_run_toplevel(final CodeRunner ctxt,
                                                     final Value handle,
                                                     final Value symbol)
        throws Fail.Exception {
        Fail.invalidArgument("caml_natdynlink_run_toplevel not implemented");
        return Value.UNIT; // never reached
    } // end method 'caml_natdynlink_run_toplevel(CodeRunner, Value, Value)'

    /**
     * No implementation of this primitive..
     * @param ctxt context
     * @param symbol ignored
     * @return <i>unit</i>
     * @throws Fail.Exception always
     */
    @Primitive
    public static Value caml_natdynlink_loadsym(final CodeRunner ctxt,
                                                final Value symbol)
        throws Fail.Exception {
        Fail.invalidArgument("caml_natdynlink_loadsym not implemented");
        return Value.UNIT; // never reached
    } // end method 'caml_natdynlink_loadsym(CodeRunner, Value)'

    /**
     * Reads the data from a stream and returns it as a byte array.
     * @param is stream to read data from
     * @return a byte array containing all the bytes from the passed stream,
     *         an empty array if the passed stream is <tt>null</tt> or if
     *         an i/o error occurs
     */
    private static byte[] readBytes(final InputStream is) {
        if (is != null) {
            try {
                final ByteArrayOutputStream baos = new ByteArrayOutputStream();
                int b = is.read();
                while (b != -1) {
                    baos.write(b);
                    b = is.read();
                } // end while
                return baos.toByteArray();
            } catch (final IOException ioe) {
                return new byte[0];
            } // end try/catch
        } else {
            return new byte[0];
        } // end if/else
    } // end method 'readBytes(InputStream)'

    /**
     * Execute the 'entry' method of a given class. <br/>
     * The class is first loaded and its constants are loaded.
     * @param runner code runner used to execute the method - should not be <tt>null</tt>
     * @param classLoader class loader used to load the class - should not be <tt>null</tt>
     * @param className name of the class - should not be <tt>null</tt>
     * @return the value returned by the 'entry' method
     * @throws Fail.Exception if the 'entry' method raises an exception
     * @throws Fail.Exception if an error occurs while trying to load the class,
     *                        or to execute the method
     */
    private static Value executeEntry(final AbstractNativeRunner runner,
                                      final ClassLoader classLoader,
                                      final String className)
        throws Fail.Exception {
        assert runner != null : "null runner";
        assert classLoader != null : "null classLoader";
        assert className != null : "null className";
        try {
            final Class<?> cl = Class.forName(className, true, classLoader);
            runner.loadConstant(cl);
            final Method m = cl.getMethod("entry", AbstractNativeRunner.class);
            return (Value) m.invoke(null, runner);
        } catch (final InvocationTargetException ite) {
            final Throwable t = ite.getTargetException();
            t.printStackTrace();
            if (t instanceof Fail.Exception) {
                throw (Fail.Exception) t;
            } else {
                Fail.invalidArgument("not an OCaml plugin");
                return Value.UNIT; // never reached
            } // end if/else
        } catch (final ClassNotFoundException cnfe) {
            Fail.invalidArgument("not an OCaml plugin");
            return Value.UNIT; // never reached
        } catch (final NoSuchMethodException nsme) {
            Fail.invalidArgument("not an OCaml plugin");
            return Value.UNIT; // never reached
        } catch (final IllegalAccessException iae) {
            Fail.invalidArgument("not an OCaml plugin");
            return Value.UNIT; // never reached
        } catch (final CadmiumException iae) {
            Fail.invalidArgument("not an OCaml plugin");
            return Value.UNIT; // never reached
        } // end try/catch
    } // end method 'executeEntry(AbstractNativeRunner, ClassLoader, String)'

    /**
     * This class is a bare structure representing a handle to a plugin.
     */
    private static final class Handle {

        /** Class loader to be used to load  plugin members. */
        private final ClassLoader classLoader;

        /** Actual plugin file. */
        private final JarFile jarFile;

        /** Map from units to fully-qualified classnames. */
        private final Properties properties;

        /**
         * Constructs a handle.
         * @param cl class loader to be used to load  plugin members - should not be <tt>null</tt>
         * @param jf actual plugin file - should not be <tt>null</tt>
         * @param p map from units to fully-qualified classnames - should not be <tt>null</tt>
         */
        private Handle(final ClassLoader cl,
                       final JarFile jf,
                       final Properties p) {
            assert cl != null : "null cl";
            assert jf != null : "null jf";
            assert p != null : "null p";
            this.classLoader = cl;
            this.jarFile = jf;
            this.properties = p;
        } // end constructor(ClassLoader, JarFile, Properties)

    } // end inner-class 'Handle'

} // end class 'NatDynlink'
